/**
 * Copyright (C) 2021-2022 maminjie <canpool@163.com>
 * SPDX-License-Identifier: LGPL-2.1
 * Powered by Qt-Advanced-Docking-System
**/

#include "docktab.h"
#include "docktab_p.h"

#include "dockmanager.h"
#include "dockmanager_p.h"
#include "dockpannel.h"
#include "dockoverlay.h"
#include "dockwidget.h"
#include "dockfloatingwidget.h"
#include "dockfloatingdragpreview.h"
#include "dockfocuscontroller.h"

#include <QApplication>
#include <QBoxLayout>
#include <QLabel>
#include <QMenu>
#include <QMouseEvent>
#include <QPushButton>
#include <QSplitter>
#include <QStyle>
#include <QToolButton>

QADS_BEGIN_NAMESPACE

class DockTabPrivate
{
public:
    DockTabPrivate(DockTab *parent);

    void init();
    void moveTab(QMouseEvent *event);

    bool isDraggingState(DragState dragState) const {
        return this->m_dragState == dragState;
    }

    bool startFloating(DragState draggingState = DraggingFloatingWidget);

    QAbstractButton *createCloseButton() const;
    void updateCloseButtonVisibility(bool active);
    void updateCloseButtonSizePolicy();

    void saveDragStartPosition(const QPoint &globalPos);

    void updateIcon();

    /**
     * Convenience function for access to the dock manager dock focus controller
     */
    DockFocusController *focusController() const
    {
        return m_dockWidget->dockManager()->dockFocusController();
    }

    template<typename T>
    DockFloatingInterface *createFloatingWidget(T *widget, bool opaqueUndocking) {
        if (opaqueUndocking) {
            return new DockFloatingWidget(widget);
        } else {
            auto w = new DockFloatingDragPreview(widget);
            QObject::connect(w, &DockFloatingDragPreview::draggingCanceled, q, [ = ]() {
                m_dragState = DraggingInactive;
            });
            return w;
        }
    }

public:
    DockTab *q;
    DockWidget *m_dockWidget = nullptr;
    QLabel *m_iconLabel = nullptr;
    DockTabLabel *m_titleLabel = nullptr;
    QAbstractButton *m_closeButton = nullptr;
    DockPannel *m_dockPannel = nullptr;
    DockFloatingInterface *m_floatingWidget = nullptr;
    QIcon m_icon;
    QSize m_iconSize;
    QPoint m_dragStartGlobalPos;
    QPoint m_dragStartPos;
    QPoint m_tabDragStartPos;
    bool m_isActive = false;
    DragState m_dragState = DraggingInactive;
}; // class DockTabPrivate

DockTabPrivate::DockTabPrivate(DockTab *parent)
    : q(parent)
{}

void DockTabPrivate::init()
{
    m_titleLabel = new DockTabLabel();
    m_titleLabel->setElideMode(Qt::ElideRight);
    m_titleLabel->setText(m_dockWidget->windowTitle());
    m_titleLabel->setObjectName("dockTabLabel");
    m_titleLabel->setAlignment(Qt::AlignCenter);
    QObject::connect(m_titleLabel, &DockTabLabel::elidedChanged, q, &DockTab::elidedChanged);

    m_closeButton = createCloseButton();
    m_closeButton->setObjectName("tabCloseButton");
    internal::setButtonIcon(m_closeButton, QStyle::SP_TitleBarCloseButton, TabCloseIcon);
    m_closeButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
    m_closeButton->setFocusPolicy(Qt::NoFocus);
    m_closeButton->setIconSize(QSize(14, 14));
    q->onDockWidgetFeaturesChanged();
    internal::setToolTip(m_closeButton, QObject::tr("Close Tab"));
    QObject::connect(m_closeButton, &QAbstractButton::clicked, q, &DockTab::closeRequested);

    QFontMetrics fontMetrics(m_titleLabel->font());
    int spacing = qRound(fontMetrics.height() / 4.0);

    // Fill the layout
    QBoxLayout *boxLayout = new QBoxLayout(QBoxLayout::LeftToRight);
    boxLayout->setContentsMargins(2 * spacing, 0, 0, 0);
    boxLayout->setSpacing(0);
    q->setLayout(boxLayout);
    boxLayout->addWidget(m_titleLabel, 1, Qt::AlignVCenter);
    boxLayout->addSpacing(spacing);
    boxLayout->addWidget(m_closeButton, 0, Qt::AlignVCenter);
    boxLayout->addSpacing(qRound(spacing * 4.0 / 3.0));
    boxLayout->setAlignment(Qt::AlignCenter | Qt::AlignVCenter);

    if (QAds::testConfigFlag(QAds::FocusHighlighting)) {
        m_closeButton->setCheckable(true);
    }

    m_titleLabel->setVisible(true);
}

void DockTabPrivate::moveTab(QMouseEvent *event)
{
    event->accept();
    QPoint distance = event->globalPos() - m_dragStartGlobalPos;
    distance.setY(0);
    auto targetPos = distance + m_tabDragStartPos;
    targetPos.rx() = qMax(targetPos.x(), 0);
    targetPos.rx() = qMin(q->parentWidget()->rect().right() - q->width() + 1, targetPos.rx());
    q->move(targetPos);
    q->raise();
}

/**
 * Starts floating of the dock widget that belongs to this title bar
 * Returns true, if floating has been started and false if floating
 * is not possible for any reason
 */
bool DockTabPrivate::startFloating(DragState draggingState)
{
    auto dockContainer = m_dockWidget->dockContainer();

    // if this is the last dock widget inside of this floating widget,
    // then it does not make any sense, to make it floating because
    // it is already floating
    if (dockContainer->isFloating() && (dockContainer->visibleDockPannelCount() == 1)
            && (m_dockWidget->dockPannel()->dockWidgetsCount() == 1)) {
        return false;
    }

    m_dragState = draggingState;
    DockFloatingInterface *floatingWidget = nullptr;
    bool opaqueUndocking = QAds::testConfigFlag(QAds::OpaqueUndocking) ||
            (draggingState != DraggingFloatingWidget);
    // If section widget has multiple tabs, we take only one tab
    // If it has only one single tab, we can move the complete
    // dock pannel into floating widget
    QSize size;

    if (m_dockPannel->dockWidgetsCount() > 1) {
        floatingWidget = createFloatingWidget(m_dockWidget, opaqueUndocking);
        size = m_dockWidget->size();
    } else {
        floatingWidget = createFloatingWidget(m_dockPannel, opaqueUndocking);
        size = m_dockPannel->size();
    }

    if (draggingState == DraggingFloatingWidget) {
        floatingWidget->startFloating(m_dragStartPos, size, DraggingFloatingWidget, q);
        auto Overlay = m_dockWidget->dockManager()->containerOverlay();
        Overlay->setAllowedAreas(OuterDockAreas);
        this->m_floatingWidget = floatingWidget;
    } else {
        floatingWidget->startFloating(m_dragStartPos, size, DraggingInactive, nullptr);
    }

    return true;
}

QAbstractButton *DockTabPrivate::createCloseButton() const
{
    if (QAds::testConfigFlag(QAds::TabCloseButtonIsToolButton)) {
        auto button = new QToolButton();
        button->setAutoRaise(true);
        return button;
    } else {
        return new QPushButton();
    }
}

/**
 * Update the close button visibility from current feature/config
 */
void DockTabPrivate::updateCloseButtonVisibility(bool active)
{
    bool dockWidgetClosable = m_dockWidget->features().testFlag(DockWidget::DockWidgetClosable);
    bool activeTabHasCloseButton = QAds::testConfigFlag(QAds::ActiveTabHasCloseButton);
    bool allTabsHaveCloseButton = QAds::testConfigFlag(QAds::AllTabsHaveCloseButton);
    bool tabHasCloseButton = (activeTabHasCloseButton && active) | allTabsHaveCloseButton;
    m_closeButton->setVisible(dockWidgetClosable && tabHasCloseButton);
}

/**
 * Update the size policy of the close button depending on the
 * RetainTabSizeWhenCloseButtonHidden feature
 */
void DockTabPrivate::updateCloseButtonSizePolicy()
{
    auto features = m_dockWidget->features();
    auto sizePolicy = m_closeButton->sizePolicy();
    sizePolicy.setRetainSizeWhenHidden(features.testFlag(DockWidget::DockWidgetClosable)
                                       && QAds::testConfigFlag(QAds::RetainTabSizeWhenCloseButtonHidden));
    m_closeButton->setSizePolicy(sizePolicy);
}

void DockTabPrivate::saveDragStartPosition(const QPoint &globalPos)
{
    m_dragStartGlobalPos = globalPos;
    m_dragStartPos = q->mapFromGlobal(globalPos);
}

void DockTabPrivate::updateIcon()
{
    if (!m_iconLabel || m_icon.isNull()) {
        return;
    }

    if (m_iconSize.isValid()) {
        m_iconLabel->setPixmap(m_icon.pixmap(m_iconSize));
    } else {
        m_iconLabel->setPixmap(m_icon.pixmap(q->style()->pixelMetric(QStyle::PM_SmallIconSize, nullptr, q)));
    }
    m_iconLabel->setVisible(true);
}

DockTab::DockTab(DockWidget *dockWidget, QWidget *parent)
    : QWidget(parent)
    , d(new DockTabPrivate(this))
{
    Q_ASSERT(dockWidget);
    setAttribute(Qt::WA_NoMousePropagation, true);
    setAttribute(Qt::WA_StyledBackground, true);
    d->m_dockWidget = dockWidget;
    d->init();

    setFocusPolicy(Qt::NoFocus);
    /*if (QAds::testConfigFlag(QAds::FocusHighlighting)) {
        setFocusPolicy(Qt::ClickFocus);
    }*/
}

DockTab::~DockTab()
{
    delete d;
}

DockWidget *DockTab::dockWidget() const
{
    return d->m_dockWidget;
}

bool DockTab::isActive() const
{
    return d->m_isActive;
}

void DockTab::setActive(bool active)
{
    d->updateCloseButtonVisibility(active);

    // Focus related stuff
    if (QAds::testConfigFlag(QAds::FocusHighlighting)
            && !d->m_dockWidget->dockManager()->isRestoringState()) {
        bool updateFocusStyle = false;
        if (active && !hasFocus()) {
            //setFocus(Qt::OtherFocusReason);
            d->focusController()->setDockTabFocused(this);
            updateFocusStyle = true;
        }

        if (d->m_isActive == active) {
            if (updateFocusStyle) {
                updateStyle();
            }
            return;
        }
    } else if (d->m_isActive == active) {
        return;
    }

    d->m_isActive = active;
    updateStyle();
    update();
    updateGeometry();
    emit activeChanged();
}

const QIcon &DockTab::icon() const
{
    return d->m_icon;
}

void DockTab::setIcon(const QIcon &icon)
{
    QBoxLayout *boxLayout = qobject_cast<QBoxLayout *>(layout());

    if (!d->m_iconLabel && icon.isNull()) {
        return;
    }

    if (!d->m_iconLabel) {
        d->m_iconLabel = new QLabel();
        d->m_iconLabel->setAlignment(Qt::AlignVCenter);
        d->m_iconLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
        internal::setToolTip(d->m_iconLabel, d->m_titleLabel->toolTip());
        boxLayout->insertWidget(0, d->m_iconLabel, Qt::AlignVCenter);
        boxLayout->insertSpacing(1, qRound(1.5 * boxLayout->contentsMargins().left() / 2.0));
    } else if (icon.isNull()) {
        // Remove icon label and spacer item
        boxLayout->removeWidget(d->m_iconLabel);
        boxLayout->removeItem(boxLayout->itemAt(0));
        delete d->m_iconLabel;
        d->m_iconLabel = nullptr;
    }
    d->m_icon = icon;
    d->updateIcon();
}

/**
 * Returns the icon size.
 * If no explicit icon size has been set, the function returns an invalid QSize
 */
QSize DockTab::iconSize() const
{
    return d->m_iconSize;
}

/**
 * Set an explicit icon size.
 * If no icon size has been set explicitely, than the tab sets the icon size
 * depending on the style
 */
void DockTab::setIconSize(const QSize &size)
{
    d->m_iconSize = size;
    d->updateIcon();
}

QString DockTab::text() const
{
    return d->m_titleLabel->text();
}

void DockTab::setText(const QString &title)
{
    d->m_titleLabel->setText(title);
}

void DockTab::setElideMode(Qt::TextElideMode mode)
{
    d->m_titleLabel->setElideMode(mode);
}

bool DockTab::isTitleElided() const
{
    return d->m_titleLabel->isElided();
}

bool DockTab::isClosable() const
{
    return d->m_dockWidget && d->m_dockWidget->features().testFlag(DockWidget::DockWidgetClosable);
}

void DockTab::setVisible(bool visible)
{
    visible &= !d->m_dockWidget->features().testFlag(DockWidget::NoTab);
    QWidget::setVisible(visible);
}

void DockTab::updateStyle()
{
    if (QAds::testConfigFlag(QAds::FocusHighlighting)) {
        if (property("focused").toBool()) {
            d->m_closeButton->setChecked(true);
        } else {
            d->m_closeButton->setChecked(false);
        }
    }

    internal::repolishStyle(this, internal::RepolishDirectChildren);
}

void DockTab::setDockPannel(DockPannel *pannel)
{
    d->m_dockPannel = pannel;
}

bool DockTab::event(QEvent *event)
{
#ifndef QT_NO_TOOLTIP
    if (event->type() == QEvent::ToolTipChange) {
        const auto text = toolTip();
        d->m_titleLabel->setToolTip(text);
        if (d->m_iconLabel) {
            d->m_iconLabel->setToolTip(text);
        }
    }
#endif
    if (event->type() == QEvent::StyleChange) {
        d->updateIcon();
    }
    return QWidget::event(event);
}

void DockTab::mousePressEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton) {
        event->accept();
        d->saveDragStartPosition(event->globalPos());
        d->m_dragState = DraggingMousePressed;
        if (QAds::testConfigFlag(QAds::FocusHighlighting)) {
            d->focusController()->setDockTabFocused(this);
        }
        emit clicked();
        return;
    }

    QWidget::mousePressEvent(event);
}

void DockTab::mouseReleaseEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton) {
        auto currentDragState = d->m_dragState;
        d->m_dragStartGlobalPos = QPoint();
        d->m_dragStartPos = QPoint();
        d->m_dragState = DraggingInactive;

        switch (currentDragState) {
        case DraggingTab:
            if (d->m_dockPannel) {
                event->accept();
                emit moved(event->globalPos());
            }
            break;
        case DraggingFloatingWidget:
            event->accept();
            d->m_floatingWidget->finishDragging();
            break;
        default:
            break; // do nothing
        }
    } else if (event->button() == Qt::MiddleButton) {
        if (QAds::testConfigFlag(QAds::MiddleMouseButtonClosesTab) &&
            d->m_dockWidget->features().testFlag(DockWidget::DockWidgetClosable)) {
            // Only attempt to close if the mouse is still
            // on top of the widget, to allow the user to cancel.
            if (rect().contains(mapFromGlobal(QCursor::pos()))) {
                event->accept();
                Q_EMIT closeRequested();
            }
        }
    }

    QWidget::mouseReleaseEvent(event);
}

void DockTab::mouseMoveEvent(QMouseEvent *event)
{
    if (!(event->buttons() & Qt::LeftButton) || d->isDraggingState(DraggingInactive)) {
        d->m_dragState = DraggingInactive;
        QWidget::mouseMoveEvent(event);
        return;
    }

    // move floating window
    if (d->isDraggingState(DraggingFloatingWidget)) {
        d->m_floatingWidget->moveFloating();
        QWidget::mouseMoveEvent(event);
        return;
    }

    // move tab
    if (d->isDraggingState(DraggingTab)) {
        // Moving the tab is always allowed because it does not mean moving the dock widget around
        d->moveTab(event);
    }

    auto mappedPos = mapToParent(event->pos());
    bool mouseOutsideBar = (mappedPos.x() < 0) || (mappedPos.x() > parentWidget()->rect().right());
    // Maybe a fixed drag distance is better here ?
    int dragDistanceY = qAbs(d->m_dragStartGlobalPos.y() - event->globalPos().y());
    if (dragDistanceY >= DockManager::startDragDistance() || mouseOutsideBar) {
        // If this is the last dock pannel in a dock container with only
        // one single dock widget it does not make sense to move it to a new
        // floating widget and leave this one empty
        if (d->m_dockPannel->dockContainer()->isFloating()
                && d->m_dockPannel->openDockWidgetsCount() == 1
                && d->m_dockPannel->dockContainer()->visibleDockPannelCount() == 1) {
            return;
        }

        // Floating is only allowed for widgets that are floatable
        // If we do non opaque undocking, then can create the drag preview
        // if the widget is movable.
        auto features = d->m_dockWidget->features();
        if (features.testFlag(DockWidget::DockWidgetFloatable)
                || (features.testFlag(DockWidget::DockWidgetMovable) && !QAds::testConfigFlag(QAds::OpaqueUndocking))) {
            // If we undock, we need to restore the initial position of this
            // tab because it looks strange if it remains on its dragged position
            if (d->isDraggingState(DraggingTab) && !QAds::testConfigFlag(QAds::OpaqueUndocking)) {
                parentWidget()->layout()->update();
            }
            d->startFloating();
        }
        return;
    } else if (d->m_dockPannel->openDockWidgetsCount() > 1
            && (event->globalPos() - d->m_dragStartGlobalPos).manhattanLength()
               >= QApplication::startDragDistance()) { // Wait a few pixels before start moving
        // If we start dragging the tab, we save its initial position to restore it later
        if (d->m_dragState != DraggingTab) {
            d->m_tabDragStartPos = this->pos();
        }

        d->m_dragState = DraggingTab;
        return;
    }

    QWidget::mouseMoveEvent(event);
}

void DockTab::mouseDoubleClickEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton) {
        // If this is the last dock area in a dock container it does not make
        // sense to move it to a new floating widget and leave this one empty
        if ((!d->m_dockPannel->dockContainer()->isFloating() || d->m_dockPannel->dockWidgetsCount() > 1)
                && d->m_dockWidget->features().testFlag(DockWidget::DockWidgetFloatable)) {
            event->accept();
            d->saveDragStartPosition(event->globalPos());
            d->startFloating(DraggingInactive);
        }
    }

    QWidget::mouseDoubleClickEvent(event);
}

void DockTab::contextMenuEvent(QContextMenuEvent *event)
{
    event->accept();

    if (d->isDraggingState(DraggingFloatingWidget)) {
        return;
    }

    d->saveDragStartPosition(event->globalPos());
    QMenu menu(this);
    const bool isFloatable = d->m_dockWidget->features().testFlag(DockWidget::DockWidgetFloatable);
    const bool isNotOnlyTabInContainer =  !d->m_dockPannel->dockContainer()->hasTopLevelDockWidget();
    const bool isDetachable = isFloatable && isNotOnlyTabInContainer;
    auto action = menu.addAction(tr("Detach"), this, &DockTab::detachDockWidget);
    action->setEnabled(isDetachable);
    menu.addSeparator();
    action = menu.addAction(tr("Close"), this, &DockTab::closeRequested);
    action->setEnabled(isClosable());
    menu.addAction(tr("Close Others"), this, &DockTab::closeOtherRequested);
    menu.exec(event->globalPos());
}

void DockTab::onDockWidgetFeaturesChanged()
{
    d->updateCloseButtonSizePolicy();
    d->updateCloseButtonVisibility(isActive());
}

void DockTab::detachDockWidget()
{
    if (!d->m_dockWidget->features().testFlag(DockWidget::DockWidgetFloatable)) {
        return;
    }

    d->saveDragStartPosition(QCursor::pos());
    d->startFloating(DraggingInactive);
}

/* DockTabLabelPrivate */
class DockTabLabelPrivate
{
public:
    DockTabLabelPrivate(DockTabLabel *parent);

    void elideText(int width);
    bool isModeElideNone() const {
        return Qt::ElideNone == m_elideMode;
    }

public:
    DockTabLabel *q;
    Qt::TextElideMode m_elideMode = Qt::ElideNone;
    QString m_text;
    bool m_isElided = false;
};

DockTabLabelPrivate::DockTabLabelPrivate(DockTabLabel *parent)
    : q(parent)
{}

void DockTabLabelPrivate::elideText(int width)
{
    if (isModeElideNone()) {
        return;
    }

    QFontMetrics fm = q->fontMetrics();
    QString str = fm.elidedText(m_text, m_elideMode, width - q->margin() * 2 - q->indent());
    if (str == u'\u2026') {
        str = m_text.at(0);
    }

    bool wasElided = m_isElided;
    m_isElided = str != m_text;
    if (m_isElided != wasElided) {
        emit q->elidedChanged(m_isElided);
    }

    q->QLabel::setText(str);
}

DockTabLabel::DockTabLabel(QWidget *parent, Qt::WindowFlags flags)
    : QLabel(parent, flags)
    , d(new DockTabLabelPrivate(this))
{}

DockTabLabel::DockTabLabel(const QString &text, QWidget *parent, Qt::WindowFlags flags)
    : QLabel(text, parent, flags)
    , d(new DockTabLabelPrivate(this))
{
    d->m_text = text;
    internal::setToolTip(this, text);
}

DockTabLabel::~DockTabLabel()
{
    delete d;
}

QString DockTabLabel::text() const
{
    return d->m_text;
}

void DockTabLabel::setText(const QString &text)
{
    d->m_text = text;

    if (d->isModeElideNone()) {
        QLabel::setText(text);
    } else {
        internal::setToolTip(this, text);
        d->elideText(this->size().width());
    }
}

Qt::TextElideMode DockTabLabel::elideMode() const
{
    return d->m_elideMode;
}

void DockTabLabel::setElideMode(Qt::TextElideMode mode)
{
    d->m_elideMode = mode;
    d->elideText(size().width());
}

bool DockTabLabel::isElided() const
{
    return d->m_isElided;
}

QSize DockTabLabel::minimumSizeHint() const
{
    if (hasPixmap() || d->isModeElideNone()) {
        return QLabel::minimumSizeHint();
    }

    const QFontMetrics &fm = fontMetrics();
#if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0))
    QSize size(fm.horizontalAdvance(d->m_text.left(2) + ".."), fm.height());
#else
    QSize size(fm.width(d->m_text.left(2) + ".."), fm.height());
#endif
    return size;
}

QSize DockTabLabel::sizeHint() const
{
    if (hasPixmap() || d->isModeElideNone()) {
        return QLabel::sizeHint();
    }

    const QFontMetrics &fm = fontMetrics();
#if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0))
    QSize size(fm.horizontalAdvance(d->m_text), QLabel::sizeHint().height());
#else
    QSize size(fm.width(d->m_text), QLabel::sizeHint().height());
#endif
    return size;
}

void DockTabLabel::mouseReleaseEvent(QMouseEvent *event)
{
    QWidget::mouseReleaseEvent(event);

    if (event->button() != Qt::LeftButton) {
        return;
    }

    emit clicked();
}

void DockTabLabel::mouseDoubleClickEvent(QMouseEvent *event)
{
    Q_UNUSED(event)
    emit doubleClicked();
    QWidget::mouseDoubleClickEvent(event);
}

void DockTabLabel::resizeEvent(QResizeEvent *event)
{
    if (!d->isModeElideNone()) {
        d->elideText(event->size().width());
    }

    QWidget::resizeEvent(event);
}

bool DockTabLabel::hasPixmap() const
{
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
    return !pixmap().isNull();
#else
    return pixmap() != nullptr;
#endif
}

QADS_END_NAMESPACE
